#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import time

from common.common_models import ActionResult
from common.const import SubJobStatusEnum, ExecuteResultEnum, RestoreTypeEnum, ReportDBLabel
from common.logger import Logger
from common.number_const import NumberConst
from db2.backup.util.db2_backup_util import Db2BackupUtil
from db2.comm.const import Db2Const, Db2JsonConstant
from db2.comm.error_code import Db2ErrCode
from db2.comm.util.common_util import Db2CommonUtil
from db2.comm.util.dpf_util import DpfUtil
from db2.comm.util.job_decorators import job_exception_decorator
from db2.comm.util.param_util import Db2ParamUtil
from db2.restore.db2_restore_base import Db2RestoreBase
from db2.restore.dpf.dpf_ts_restore_service import DpfTsRestoreService

LOGGER = Logger().get_logger(filename="db2.log")


class DpfTsRestore(Db2RestoreBase):
    """Db2表空间集恢复任务执行类
    """

    def __init__(self, task_name, pid, job_id, sub_job_id, param_dict):
        super().__init__(task_name, pid, job_id, sub_job_id, param_dict)
        self._os_user = Db2ParamUtil.get_tgt_os_user_when_restore(self.pid)
        self._db_name = Db2ParamUtil.get_db_name_when_restore_ts(self.param_dict)
        self._restore_type = Db2ParamUtil.get_restore_type(self.param_dict)

    @staticmethod
    def _build_action_result(err_param):
        return Db2CommonUtil.build_action_result(
            code=ExecuteResultEnum.INTERNAL_ERROR.value,
            message="There are tablespaces whose state is not normal in the backup tablespaces",
            body_err=Db2ErrCode.SPACE_STATUS_ABNORMAL, err_params=[err_param])

    @job_exception_decorator(is_restore=True)
    def allow_restore_in_local_node(self):
        tgt_ts_names = Db2ParamUtil.get_restore_table_spaces(self.param_dict)
        src_ts_names = Db2ParamUtil.get_table_spaces_of_ts_copy(self.param_dict)
        if not tgt_ts_names or not src_ts_names or set(tgt_ts_names) != set(src_ts_names):
            LOGGER.error(f"The tablespaces are inconsistent or do not exist, source "
                         f"tablespaces: {src_ts_names}, target tablespaces: {tgt_ts_names}, job id: {self.job_id}.")
            err_param = DpfUtil.build_table_space_not_exists_message(src_ts_names, tgt_ts_names)
            if not err_param:
                err_param = DpfUtil.build_table_space_not_exists_message(tgt_ts_names, src_ts_names)
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value,
                message="The tablespaces are inconsistent or do not exist",
                body_err=Db2ErrCode.SPACE_NOT_EXISTS, err_params=[err_param])
        # 检查源表空间集库所属实例和目标表空间集所属实例是否一致
        src_inst_name = Db2ParamUtil.get_inst_name_of_ts_copy(self.param_dict)
        tgt_inst_name = Db2ParamUtil.get_tgt_inst_name_when_restore_ts(self.param_dict)
        if not tgt_inst_name or not src_inst_name or tgt_inst_name != src_inst_name:
            LOGGER.error(f"During restore tablespaces, the instance name is inconsistent or does not exist, source "
                         f"instance: {src_inst_name}, target instance: {tgt_inst_name}, job id: {self.job_id}.")
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value,
                message="The instance name is inconsistent or does not exist",
                body_err=Db2ErrCode.DB_INSTANCE_NOT_EQUAL, err_params=[src_inst_name, tgt_inst_name])
        # 检查源表空间集库所属数据库和目标表空间集所属数据库是否一致
        src_db_name = Db2ParamUtil.get_db_name_of_ts_copy(self.param_dict)
        if not self._db_name or not src_db_name or self._db_name != src_db_name:
            LOGGER.error(f"During restore tablespaces, the database name is inconsistent or does not exist, source "
                         f"database: {src_db_name}, target database: {self._db_name}, job id: {self.job_id}.")
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, body_err=Db2ErrCode.SPACE_OF_DB_NOT_EQUAL,
                message="The database name is inconsistent or does not exist", err_params=[src_db_name, self._db_name])
        # 检查DPF集群各节点状态是否正常
        connect_status = Db2CommonUtil.check_process_service(self._os_user)
        if not connect_status:
            LOGGER.error(f"Check dpf instance connection result: {connect_status}.")
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, message="The DPF cluster status is abnormal",
                body_err=Db2ErrCode.DB_SERVICE_ERROR)

        pending_ret, pending_info_str = DpfUtil.check_database_pending_status_for_restore(self._os_user, self._db_name)
        # 检查目标数据库状态是否正常
        if pending_ret:
            LOGGER.error("There are pending tasks in the db2 single database .")
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, body_err=Db2ErrCode.DATABASE_EXISTS_PENDING,
                err_params=[pending_info_str], message="There are pending tasks in the db2 single database")

        # 检查表空间状态
        ret, action_result = self.check_tablespace_status()
        if not ret:
            return action_result

        # 检查表状态
        ret, action_result = self.check_table_status()
        if not ret:
            return action_result
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    @job_exception_decorator(write_progress=True, is_restore=True)
    def exec_restore_pre_job(self):
        LOGGER.info(f"Start executing restore tablespaces pre job, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub jod id: {self.sub_job_id}.")
        # 节点数目、分片必须一致
        self.sub_job_detail.progress = NumberConst.HUNDRED
        self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        LOGGER.info(f"Execute restore tablespaces pre job finished, pid: {self.pid}, job id: {self.job_id}")
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    @job_exception_decorator(write_progress=True, is_restore=True)
    def exec_restore(self):
        s_job_name = Db2ParamUtil.get_sub_job_name_for_restore(self.param_dict)
        tmp_job_log = f"sub job: {s_job_name}, pid: {self.pid}, job id: {self.job_id}, sub job id: {self.sub_job_id}"
        LOGGER.info(f"Start executing restore tablespaces job, {tmp_job_log}.")
        self.sub_job_detail.log_detail = [
            Db2CommonUtil.build_log_detail(ReportDBLabel.RESTORE_SUB_START_COPY, [self.sub_job_id])
        ]
        # 挂载目录子任务，确保执行后续子任务时各节点已成功挂载目录
        if s_job_name == "mount_path_subtask":
            return self.exec_mount_path_subtask()

        self.sub_job_detail.progress = NumberConst.FIFTEEN
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        time.sleep(Db2Const.LABEL_SLEEP_TIME)
        self.sub_job_detail.log_detail = []
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        # 获取编目节点IP
        catalog_ip = Db2ParamUtil.get_catalog_ip_when_restore(self.param_dict)
        if not catalog_ip:
            return self.handle_empty_catalog_ip()
        # 非编目节点不做恢复操作
        if not DpfUtil.is_catalog_node(catalog_ip):
            LOGGER.info("Current node is no catalog node, no need execute restore tablespaces job.")
            Db2CommonUtil.disconnect_db_application(self._os_user, self._db_name)
            self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
            Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
            return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

        res_ts_names = Db2ParamUtil.get_restore_table_spaces(self.param_dict)
        if self._restore_type == RestoreTypeEnum.FULL_RESTORE.value:
            DpfTsRestoreService.handle_full_restore_ts(self._os_user, self._db_name, res_ts_names, self.param_dict)
        elif self._restore_type == RestoreTypeEnum.INCRE_RESTORE.value:
            DpfTsRestoreService.handle_incr_restore_ts(self._os_user, self._db_name, res_ts_names, self.param_dict)
        elif self._restore_type == RestoreTypeEnum.DIFF_RESTORE.value:
            DpfTsRestoreService.handle_diff_restore_ts(self._os_user, self._db_name, res_ts_names, self.param_dict)
        else:
            LOGGER.error(f"Unsupported restore type: {self._restore_type}.")
            self.sub_job_detail.task_status = SubJobStatusEnum.FAILED.value
            Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
            return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                                     message="Unsupported backup type")
        # 恢复后检查恢复表空间状态是否正常
        all_ts_dict = Db2CommonUtil.get_table_space_info_dict_of_db(self._os_user, self._db_name)
        chk_bak_ts_state_ret = list(all_ts_dict.get(i, {}).get("state") == Db2Const.NORMAL_STATE for i in res_ts_names)
        if not all(chk_bak_ts_state_ret):
            LOGGER.error(f"There is an abnormal state in the restored tablespaces after restoring, "
                         f"database name: {self._db_name}, restored tablespaces: {res_ts_names}.")
            self.sub_job_detail.task_status = SubJobStatusEnum.FAILED.value
            self.sub_job_detail.progress = NumberConst.NINETY
            Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
            return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                                     message="Unsupported backup type")
        self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
        self.sub_job_detail.progress = NumberConst.HUNDRED
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        LOGGER.info(f"Execute restore tablespaces job finished, {tmp_job_log}.")
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    def handle_empty_catalog_ip(self):
        LOGGER.error("The catalog IP param is empty.")
        self.sub_job_detail.task_status = SubJobStatusEnum.FAILED.value
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                                 message="The catalog IP param is empty")

    def exec_mount_path_subtask(self):
        Db2CommonUtil.disconnect_db_application(self._os_user, self._db_name)
        self.sub_job_detail.progress = NumberConst.TEN
        self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    @job_exception_decorator(write_progress=True, is_restore=True)
    def exec_restore_post_job(self):
        LOGGER.info(f"Start executing restore tablespaces post job, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub jod id: {self.sub_job_id}.")
        self.sub_job_detail.progress = NumberConst.FIVE
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        # 恢复后置任务
        if self._restore_type in (RestoreTypeEnum.INCRE_RESTORE.value, RestoreTypeEnum.DIFF_RESTORE.value):
            copies = Db2ParamUtil.parse_copies(self.param_dict)
            full_copy_path = Db2ParamUtil.get_full_copy_path_for_incr_and_diff_restore(copies, self.job_id)
            Db2CommonUtil.clear_soft_link_of_dir(full_copy_path)
            LOGGER.info(f"Remove soft link of tablespaces full copy success, restore type: {self._restore_type}.")
        self.sub_job_detail.progress = NumberConst.HUNDRED
        self.sub_job_detail.task_status = SubJobStatusEnum.COMPLETED.value
        Db2CommonUtil.proactively_report_progress(self.pid, self.sub_job_detail)
        LOGGER.info(f"Execute restore tablespaces post job finished, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub job id: {self.sub_job_id}.")
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    def report_progress(self):
        cache_path = Db2ParamUtil.get_cache_mount_path_for_restore(self.param_dict)
        if self.sub_job_id:
            Db2BackupUtil.report_progress_utl(self.pid, self.sub_job_id, cache_path)
        else:
            Db2BackupUtil.report_progress_utl(self.pid, self.job_id, cache_path)

    def output_action_result(self, action_ret: ActionResult):
        super(DpfTsRestore, self).output_action_result(action_ret)

    def check_tablespace_status(self):
        db_name = Db2ParamUtil.get_db_name_of_ts_copy(self.param_dict)
        tgt_ts_names = Db2ParamUtil.get_ts_name_for_restore(self.param_dict)
        tablespace_info = Db2ParamUtil.get_ts_info_for_restore(self.param_dict)
        tablespace_set = Db2CommonUtil.check_tablespace_status(self._os_user, db_name, tgt_ts_names, tablespace_info)
        if tablespace_set:
            ts_names = ",".join(dict(tablespace_set).keys())
            LOGGER.error("The table spaces of the database does not exists.")
            return False, Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value,
                message="The table spaces of the database does not exists",
                body_err=Db2ErrCode.SPACE_NOT_EXISTS, err_params=[ts_names])
        return True, Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    def check_table_status(self):
        db_name = Db2ParamUtil.get_db_name_of_ts_copy(self.param_dict)
        tgt_ts_names = Db2ParamUtil.get_ts_name_for_restore(self.param_dict)
        table_info = Db2ParamUtil.get_table_info_for_restore(self.param_dict)
        table_set = Db2CommonUtil.check_table_status(self._os_user, db_name, tgt_ts_names, table_info)
        if table_set:
            LOGGER.error("Table is delete in backup.")
            table_names = list(dict(table_set).keys())
            table_spaces_str = self.param_dict.get("job", {}).get("targetObject", {}). \
                get("extendInfo", {}).get("table", "")
            return False, Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, message="Table is deleted!",
                body_err=Db2ErrCode.TABLESPACES_DROP_TABLE,
                err_params=[table_names[0], table_spaces_str, table_names[0]])
        return True, Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)

    def _check_table_in_space(self):
        # 表空间下表被删除掉
        src_db_name = Db2ParamUtil.get_db_name_of_ts_copy(self.param_dict)
        tgt_ts_names = Db2ParamUtil.get_ts_name_for_restore(self.param_dict)
        table_name_drop = []
        table_from_restore, table_in_restore = Db2CommonUtil.check_table_from_tablespace(self._os_user,
                                                                                         src_db_name, tgt_ts_names)
        path = Db2ParamUtil.get_copy_path_info(self.param_dict)
        backup_table = path[0].get(Db2JsonConstant.BACKUP_TABLE)
        for table_name in backup_table:
            if table_name not in table_in_restore:
                table_name_drop.append(table_name)
        if table_name_drop:
            LOGGER.error("Table is delete in backup!")
            table_spaces_str = ",".join(Db2ParamUtil.get_restore_table_spaces(self.param_dict))
            return Db2CommonUtil.build_action_result(
                code=ExecuteResultEnum.INTERNAL_ERROR.value, message="Table is deleted!",
                body_err=Db2ErrCode.TABLESPACES_DROP_TABLE,
                err_params=[table_name_drop[0], table_spaces_str, table_name_drop[0]])
        LOGGER.info(f"Execute restore tablespaces post job finished, pid: {self.pid}, job id: {self.job_id}, "
                    f"sub job id: {self.sub_job_id}.")
        return Db2CommonUtil.build_action_result(code=ExecuteResultEnum.SUCCESS.value)
